﻿namespace JoinBox.WPF;

using System;
using System.Reflection.Emit;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;


/// <summary>
/// 事件绑定标签类
/// </summary>
/// <seealso cref="MarkupExtension" />
/// MarkupExtension引用问题见: https://www.cnblogs.com/JJBox/p/12677496.html
public class EventBindingExtension : MarkupExtension
{
    /// <summary>
    /// 命令属性
    /// </summary>
    public string? Command;
    /// <summary>
    /// 命令参数属性
    /// </summary>
    public string? CommandParameter;
    /// <summary>
    /// 当在派生类中实现时,返回用作此标记扩展的目标属性值的对象。
    /// </summary>
    /// <param name="serviceProvider">可为标记扩展提供服务的服务提供程序帮助程序。</param>
    /// <returns>
    /// 要在应用了扩展的属性上设置的对象值。
    /// </returns>
    /// <exception cref="InvalidOperationException">
    /// </exception>
    public override object? ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider is null)
            throw new ArgumentNullException("ProvideValue:" + nameof(serviceProvider));

        var ipvt = serviceProvider.GetService(typeof(IProvideValueTarget));
        if (ipvt is not IProvideValueTarget targetProvider)
            throw new InvalidOperationException();

        if (targetProvider.TargetObject is not FrameworkElement targetObject)
            throw new InvalidOperationException();

        if (targetProvider.TargetProperty is not MemberInfo memberInfo)
            throw new InvalidOperationException();

#if NET35
        bool isNull = string.IsNullOrEmpty(Command) && string.IsNullOrEmpty(Command?.Trim());
#else
        bool isNull = string.IsNullOrWhiteSpace(Command);
#endif
        if (isNull)
        {
            Command = memberInfo.Name.Replace("Add", "");
            if (Command.Contains("Handler"))
                Command = Command.Replace("Handler", "Command");
            else
                Command += "Command";
        }

        if (Command is null)
            return null;

        return CreateHandler(memberInfo, Command, targetObject.GetType());
    }

    static Type? GetEventHandlerType(MemberInfo memberInfo)
    {
        Type? eventHandlerType = null;
        if (memberInfo is EventInfo)
        {
            var info = memberInfo as EventInfo;
            eventHandlerType = info?.EventHandlerType;
        }
        else if (memberInfo is MethodInfo)
        {
            var info = memberInfo as MethodInfo;
            var pars = info?.GetParameters();
            eventHandlerType = pars?[1].ParameterType;
        }
        return eventHandlerType;
    }

#pragma warning disable IDE0060 // 删除未使用的参数
    private object? CreateHandler(MemberInfo memberInfo, string cmdName, Type targetType)
#pragma warning restore IDE0060 // 删除未使用的参数
    {
        var eventHandlerType = GetEventHandlerType(memberInfo);
        if (eventHandlerType is null)
            return null;

        var handlerInfo = eventHandlerType.GetMethod("Invoke");
        var method = new DynamicMethod("", handlerInfo.ReturnType,
            new Type[]
            {
                handlerInfo.GetParameters()[0].ParameterType,
                handlerInfo.GetParameters()[1].ParameterType,
            });

        var gen = method.GetILGenerator();
        gen.Emit(OpCodes.Ldarg, 0);
        gen.Emit(OpCodes.Ldarg, 1);
        gen.Emit(OpCodes.Ldstr, cmdName);
        if (CommandParameter is null)
            gen.Emit(OpCodes.Ldnull);
        else
            gen.Emit(OpCodes.Ldstr, CommandParameter);
        gen.Emit(OpCodes.Call, getMethod);
        gen.Emit(OpCodes.Ret);

        return method.CreateDelegate(eventHandlerType);
    }

    static readonly MethodInfo getMethod = typeof(EventBindingExtension).GetMethod("HandlerIntern",
        new Type[] { typeof(object), typeof(object), typeof(string), typeof(string) });

    // static void Handler(object sender, object args)
    // {
    //    HandlerIntern(sender, args, "cmd", null);
    // }
    /// <summary>
    /// 处理者 the intern.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="args">The arguments.</param>
    /// <param name="cmdName">Name of the command.</param>
    /// <param name="commandParameter">The command parameter.</param>
    public static void HandlerIntern(object sender, object args, string cmdName, string? commandParameter)
    {
        if (sender is not FrameworkElement fe)
            return;

        var cmd = GetCommand(fe, cmdName);
        object? commandParam = null;

#if NET35
        bool isNull = string.IsNullOrEmpty(commandParameter)
                && string.IsNullOrEmpty(commandParameter?.Trim());
#else
        bool isNull = string.IsNullOrWhiteSpace(commandParameter);
#endif
        if (!isNull)
            commandParam = GetCommandParameter(fe, args, commandParameter);
        if (cmd is not null && cmd.CanExecute(commandParam))
            cmd.Execute(commandParam);
    }

    internal static ICommand? GetCommand(FrameworkElement target, string cmdName)
    {
        var vm = FindViewModel(target);
        if (vm is null)
            return null;

        var vmType = vm.GetType();
        var cmdProp = vmType.GetProperty(cmdName);
        if (cmdProp is not null)
            return cmdProp.GetValue(vm, null) as ICommand;
#if DEBUG
        throw new Exception("EventBinding path error: '" + cmdName + "' property not found on '" + vmType + "' 'DelegateCommand'");
#else
        return null;
#endif
    }

    internal static object? GetCommandParameter(FrameworkElement target, object args, string? commandParameter)
    {
        if (commandParameter is null)
            return commandParameter;

        var classify = commandParameter.Split('.');
        object result = classify[0] switch
        {
            "$e" => args,
            "$this" => classify.Length > 1 ? FollowPropertyPath(target, commandParameter.Replace("$this.", ""), target.GetType()) : target,
            _ => commandParameter,
        };
        return result;
    }

    internal static ViewModelBase? FindViewModel(FrameworkElement? target)
    {
        if (target is null)
            return null;

        if (target.DataContext is ViewModelBase vm)
            return vm;

        var parent = target.GetParentObject() as FrameworkElement;

        return FindViewModel(parent);
    }

    internal static object FollowPropertyPath(object target, string path, Type? valueType = null)
    {
        if (target is null)
            throw new ArgumentNullException(nameof(target));
        if (path is null)
            throw new ArgumentNullException(nameof(path));

        var currentType = valueType ?? target.GetType();

        foreach (string propertyName in path.Split('.'))
        {
            var property = currentType.GetProperty(propertyName);
            if (property is null)
                throw new NullReferenceException("FollowPropertyPath:" + "property");

            target = property.GetValue(target, null);
            currentType = property.PropertyType;
        }
        return target;
    }
}
